Build better forms with React hook form | Everything you need to know

In this article, we will learn how to build better and more performant forms with the React hook form.

We will learn:

  • Controlled vs Uncontrolled inputs
  • Why React hook form?
  • Basic usage
  • Form validation
  • Form Context

You can also watch the crash course where I have explained everything in detail.

Controlled vs Uncontrolled inputs

A controlled input is an input whose value is controlled by React. In other words, the value of the input is stored in the state of the component and is updated via the onChange handler.

1import React, { useState } from 'react'
2
3function ControlledInput() {
4 const [inputValue, setInputValue] = useState('')
5
6 const handleInputChange = event => {
7 setInputValue(event.target.value)
8 }
9
10 return <input type='text' value={inputValue} onChange={handleInputChange} />
11}

On the other hand, an uncontrolled input is an input whose value is not controlled by React. In other words, the value of the input is stored in the DOM and is updated via the ref attribute.

1import React, { useRef } from 'react'
2
3function UncontrolledInput() {
4 const inputRef = useRef()
5
6 const handleButtonClick = () => {
7 alert(`Input value: ${inputRef.current.value}`)
8 }
9
10 return (
11 <div>
12 <input type='text' ref={inputRef} />
13 <button onClick={handleButtonClick}>Get Value</button>
14 </div>
15 )
16}

React state rerender the component whenever the state changes. So, if we have a form with many inputs, then it will rerender the component whenever the user types something in the input field. This will cause performance issues. But, with uncontrolled inputs, we can avoid this issue because ref doesn't cause the component to rerender.

Why React hook form?

  1. Performance(because of uncontrolled inputs)
  2. Easy to use
  3. Easy to integrate with UI libraries like Material UI, Chakra UI, etc.
  4. Validation out of the box or integration with Yup, Joi, etc.

Basic usage

You can install the react-hook-form package in your project. I am going to use Chakra UI for styling. You can use any UI library or your custom styles.

Registering inputs

This library works by registering inputs to the form using a hook called useForm

1import { useForm } from 'react-hook-form'
2
3const { register } = useForm()

The register function is used to register the input to the form. And we need to call it to all the input and spread the return object. The first argument has to be

1<Input id='name' placeholder='Name' {...register('name')} />

Submitting the form

To submit the form, we need to call the handleSubmit function from the useForm hook.

1const {
2 handleSubmit,
3 formState: { isSubmitting }, // A state for displaying loading indicator
4} = useForm()
5
6const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
7
8// submit handler
9const onSubmit = async data => {
10 await sleep(2000)
11 if (data) {
12 alert(JSON.stringify(data))
13 } else {
14 alert('There is an error')
15 }
16}
17
18const MyForm = () => {
19 return (
20 <form onSubmit={handleSubmit(onSubmit)}>
21 {/* form inputs */}
22 {/* more form inputs */}
23 <Button type='submit' isLoading={isSubmitting}>
24 Submit
25 </Button>
26 </form>
27 )
28}

Explanation:

  1. We are using the sleep function to simulate an API call.
  2. The handleSubmit function takes a callback function as an argument. This callback function will be called when the form is submitted.
  3. The handleSubmit function will pass the form data to the callback function as an argument and we will display the data.

Default values

You can add default values to the form using the defaultValues prop of the useForm hook.

1const { register } = useForm({
2 defaultValues: {
3 name: 'Jane',
4 gender: 'female',
5 email: 'Jane@gmail.om',
6 password: '123456',
7 },
8})

Getting form values

You can do it in two ways. Using the watch function or using the getValues function.

1const { watch, getValues } = useForm()
2
3watch('name') // watch a single input
4watch(['name', 'email']) // watch multiple inputs
5watch() // watch all inputs
6
7getValues('name') // watch a single input
8getValues(['name', 'email']) // watch multiple inputs
9getValues() // watch all inputs

Explanation:

  1. The watch function will cause a rerender of the component where it is called whenever the value of the input changes. Similar to the react state. Use the useWatch hook for reducing rerenders.
  2. The getValues function will return the value of the input. It will not cause a rerender. You want to use this inside an event handler like onClick.

You can also add onChange handlers to inputs.

1const { register } = useForm()
2
3<Input
4 id='name'
5 placeholder='Name'
6 {...register('name', {
7 onChange: e => console.log(e.target.value),
8 })}
9/>

Form validation

You can validate the form using the register function as a second parameter.

1const Myform = () => {
2 const { register, errors } = useForm()
3 return (
4 <form>
5 <FormControl isInvalid={errors.name}>
6 <FormLabel htmlFor='name'>Name</FormLabel>
7 <Input
8 id='name'
9 placeholder='Name'
10 {...register('name', {
11 required: 'This field is required',
12 minLength: {
13 value: 10,
14 message: 'Minimum length should be 10',
15 },
16 })}
17 />
18 <FormErrorMessage>{errors.name && errors.name.message}</FormErrorMessage>
19 </FormControl>
20 <FormControl isInvalid={errors.gender}>
21 <FormLabel htmlFor='gender'>Gender</FormLabel>
22 <Select
23 placeholder='Gender'
24 {...register('gender', { required: 'This field is required' })}
25 >
26 <option value='male'>Male</option>
27 <option value='female'>Female</option>
28 </Select>
29 <FormErrorMessage>
30 {errors.gender && errors.gender.message}
31 </FormErrorMessage>
32 </FormControl>
33 </form>
34 )
35}

Explanation:

  1. The errors object will contain all the errors of the form. You can use it to display the error message.
  2. The isInvalid prop of the FormControl component will display the error message if the input is invalid. Only needed if you use Chakra UI.

Learn more about validation from here.

Form State

You can get the form state using the formState object coming from useform.

1const { formState } = useForm()

Some of the useful Properties:

  • isDirty
  • isSubmitSuccessful
  • isSubmitting
  • isValid
  • errors
  • dirtyFields

Learn more about form state from here.

Form Context

You can create a form context which is a global state using the useFormContext hook. This can be useful when you have nested forms or you are trying to build multi-step forms.

  • Wrapper component
1const App = () => {
2 const formMethods = useForm({
3 defaultValues: {
4 companyName: 'Google',
5 },
6 })
7
8 return (
9 <FormProvider {...formMethods}>
10 <MyFormWithContext />
11 <MyForm />
12 </FormProvider>
13 )
14}
  • Child component
1const {
2 handleSubmit,
3 formState: { errors, isSubmitting, isValid },
4 register,
5 } = useFormContext()
6
7 const Form () => {
8 // components
9 }

Explanation:

  • We are using the FormProvider component to wrap the form components and spread the return object of the useForm hook.
  • We are using the useFormContext hook to get the form context in the child component. It will return the same object as the useForm hook.
  • The process is very similar to react context.

Learn more about form context from here.

To learn more about this, I would recommend checking my crash course.

Shameless Plug

I have made an Xbox landing page clone with React and Styled components. I hope you will enjoy it. Please consider like this video and subscribe to my channel.

That's it for this blog. I have tried to explain things simply. If you get stuck, you can ask me questions.

Contacts

Blogs you might want to read:

Videos might you might want to watch:

Previous PostRadix UI crash course
Next PostHow to setup and deploy a fullStack(mern) application on Vercel and Render